/*
 * @Author: 13177709051 3506463075@qq.com
 * @Date: 2024-10-19 00:06:40
 * @LastEditors: 13177709051 3506463075@qq.com
 * @LastEditTime: 2024-10-19 20:26:47
 * @FilePath: /linux12/myshell.cc
 * @Description: 这是默认设置,请设置`customMade`, 打开koroFileHeader查看配置 进行设置: https://github.com/OBKoro1/koro1FileHeader/wiki/%E9%85%8D%E7%BD%AE
 */
#include <iostream>
#include <cstdio>
#include <cstdlib>
#include <cstring>
#include <string>
#include <unistd.h>
#include <sys/types.h>
#include <sys/wait.h>

using namespace std;

const int basesize = 1024;
const int argvnum = 64;
const int envnum = 64;
// 全局的命令行参数表
char *gargv[argvnum];
int gargc = 0;

// 全局的变量
int lastcode = 0;

// 我的系统的环境变量
char *genv[envnum];

// 全局的当前shell工作路径
char pwd[basesize];
char pwdenv[basesize];

// basesize: 定义了缓冲区的基础大小，用于存储用户输入的命令和路径。
// argvnum: 定义最大命令行参数的数量。
// envnum: 定义最大环境变量的数量。
// gargv: 存储解析后的命令行参数的数组。
// gargc: 表示当前解析出的参数数量。
// lastcode: 保存最近执行命令的退出状态。
// genv: 存储环境变量的数组。
// pwd 和 pwdenv: 存储当前工作目录和环境变量PWD的缓冲区。

// 环境信息获取

string GetUserName()
{
    string name = getenv("USER");
    return name.empty() ? "None" : name;
}

string GetHostName()
{
    string hostname = getenv("HOSTNAME");
    return hostname.empty() ? "None" : hostname;
}

string GetPwd()
{
    if (nullptr == getcwd(pwd, sizeof(pwd)))
        return "None";
    snprintf(pwdenv, sizeof(pwdenv), "PWD=%s", pwd);
    putenv(pwdenv); // PWD=XXX
    return pwd;

    // string pwd = getenv("PWD");
    // return pwd.empty() ? "None" : pwd;
}

string LastDir()
{
    string curr = GetPwd();
    if (curr == "/" || curr == "None")
        return curr;
    // /home/whb/XXX
    size_t pos = curr.rfind("/");
    if (pos == std::string::npos)
        return curr;
    return curr.substr(pos + 1);
}

// GetUserName: 获取当前用户的用户名，使用getenv从环境变量中获取，若未获取到，则返回"None"。
// GetHostName: 获取主机名，类似于获取用户名的方式。
// GetPwd: 获取当前工作目录，使用getcwd函数获取并更新环境变量PWD。如果获取失败，返回"None"。
// LastDir: 从当前路径中提取最后一个目录名称，方便在命令行提示符中显示。

// snprintf 是一个安全的字符串格式化函数，用于将格式化的数据写入字符串。
// pwdenv 是另一个字符数组，用于存储更新后的环境变量字符串。
// 这里，"PWD=%s" 是格式化字符串，%s 被当前工作目录的值（即 pwd 中的值）替换。最终结果类似于 PWD=/home/user（假设当前工作目录是 /home/user）。

// putenv 是一个函数，用于设置环境变量。
// 通过调用 putenv(pwdenv)，将之前构造的环境变量字符串 PWD=/path/to/current/dir 添加到进程的环境中。
// 注意，使用 putenv 要确保 pwdenv 的内容在后续使用中依然有效，因为 putenv 不会复制传入的字符串。

// 命令行提示符
// 功能：生成和打印命令行提示符，包含用户、主机和当前目录信息。

string MakeCommandLine()
{
    // [whb@bite-alicloud myshell]$
    char command_line[basesize];
    snprintf(command_line, basesize, "[%s@%s %s]# ",
             GetUserName().c_str(), GetHostName().c_str(), LastDir().c_str());
    return command_line;
}

// 命令行提示符
void PrintCommandLine() // 1. 命令行提示符
{
    printf("%s", MakeCommandLine().c_str());
    fflush(stdout);
}

// MakeCommandLine: 生成当前命令行提示符的字符串格式，包括用户、主机和当前目录。
// PrintCommandLine: 打印生成的命令行提示符到标准输出。

// 命令行输入处理
//  功能：从标准输入获取用户输入的命令，并解析成命令及其参数。

// 获取用户命令
bool GetCommandLine(char command_buffer[], int size) // 2. 获取用户命令
{
    // 我们认为：我们要将用户输入的命令行，当做一个完整的字符串
    // "ls -a -l -n"
    char *result = fgets(command_buffer, size, stdin);
    if (!result)
    {
        return false;
    }
    command_buffer[strlen(command_buffer) - 1] = 0;
    if (strlen(command_buffer) == 0)
        return false;
    return true;
}

// fgets 是一个标准库函数，用于从标准输入（stdin）读取一行字符，最多读取 size - 1 个字符，并将其存储在 command_buffer 中。
// fgets 会在读取到换行符 ('\n') 或 EOF（文件结束符）时停止读取，并会在 command_buffer 的末尾自动添加一个空字符 ('\0')。
// 如果成功读取，result 将指向 command_buffer；如果失败（例如，输入错误或 EOF），result 将为 nullptr。

// strlen(command_buffer) - 1 获取字符串的最后一个字符（换行符）的索引，并将其设为 0，以去掉换行符。

// 分析命令
void ParseCommandLine(char command_buffer[], int len) // 3. 分析命令
{
    (void)len;
    memset(gargv, 0, sizeof(gargv));
    gargc = 0;
    // "ls -a -l -n"
    const char *sep = " ";
    gargv[gargc++] = strtok(command_buffer, sep);
    // =是刻意写的
    while((bool)(gargv[gargc++] = strtok(nullptr, sep)));
    gargc--;
}
// GetCommandLine: 从标准输入获取用户的命令行输入，使用fgets读取并处理换行符。
// ParseCommandLine: 使用strtok将输入的命令行字符串分割为单独的命令和参数，并存储在gargv数组中。

// strtok 函数会将 command_buffer 字符串中的第一个分隔符（空格）替换为 \0，并返回指向第一个标记的指针，存储到 gargv 数组中。
// strtok(nullptr, sep) 意味着继续解析之前传入的字符串。
// 注意到 (bool) 转换是为了确保表达式的结果为布尔值，这样可以明确表示循环的条件。

// 命令调试
// 功能：输出当前命令参数的数量和每个参数的值，以帮助开发和调试。
void debug()
{
    printf("argc: %d\n", gargc);
    for (int i = 0; gargv[i]; i++)
    {
        printf("argv[%d]: %s\n", i, gargv[i]);
    }
}

// 命令执行
// 功能：通过fork()和execvpe()创建子进程执行用户命令，并等待其完成。

bool ExecuteCommand() // 4. 执行命令
{
    // 让子进程进行执行
    pid_t id = fork();
    if (id < 0)
        return false;
    if (id == 0)
    {
        // 子进程
        //  1. 执行命令
        execvpe(gargv[0], gargv, genv);
        // 2. 退出
        exit(1);
    }
    int status = 0;
    pid_t rid = waitpid(id, &status, 0);
    if (rid > 0)
    {
        if (WIFEXITED(status))
        {
            lastcode = WEXITSTATUS(status);
        }
        else
        {
            lastcode = 100;
        }
        return true;
    }
    return false;
}

// execvpe(gargv[0], gargv, genv);：execvpe  用于执行指定的命令。它会用新的程序替换当前进程（子进程）。参数说明：
// gargv[0]：命令的名称（通常是命令的路径）。
// gargv：一个字符串数组，包含传递给命令的参数。
// genv：一个字符串数组，包含环境变量。
// 如果 execvpe 成功，当前进程（子进程）将被新的程序替换，之后的代码将不会执行。
// 如果 execvpe 失败（例如命令未找到），子进程将执行 exit(1)，返回状态 1，以指示错误。

// int status = 0;：用于存储子进程的退出状态。
// waitpid(id, &status, 0)：在父进程中调用，等待指定的子进程（由 id 指定）结束。结果存储在 status 变量中。
// 返回值 rid：如果成功，返回已结束子进程的 PID；如果失败，返回 -1。

// if(rid > 0)：确认 waitpid 调用成功，rid 应该是大于 0 的子进程 PID。
// WIFEXITED(status)：这个宏检查子进程是否正常退出（通过 exit() 或 return 语句）。
// WEXITSTATUS(status)：如果子进程正常退出，获取其返回状态并将其存储在 lastcode 中。
// 如果子进程未正常退出（例如由于信号导致的终止），则将 lastcode 设置为 100，这通常用于指示异常情况。
// return true;：表示成功执行命令并处理了其退出状态。

// 环境变量管理
//  功能：动态分配内存，将新的环境变量添加到genv数组中。

void AddEnv(const char *item)
{
    int index = 0;
    while (genv[index])
    {
        index++;
    }

    genv[index] = (char *)malloc(strlen(item) + 1);
    strncpy(genv[index], item, strlen(item) + 1);
    genv[++index] = nullptr;
}

// 功能
// 添加环境变量：该函数接受一个字符串 item 作为参数，并将其添加到 genv 环境变量数组中。
// 逐行分析
// int index = 0;：初始化索引为 0，用于遍历 genv 数组。

// while(genv[index])：遍历 genv 数组，直到找到第一个空指针，表示可以插入新环境变量的位置。

// genv[index] = (char*)malloc(strlen(item) + 1);：

// 为新的环境变量分配内存。malloc 分配的内存大小为 item 字符串的长度加 1（为字符串结束符 \0 留出空间）。
// strncpy(genv[index], item, strlen(item) + 1);：

// 将 item 的内容复制到 genv[index] 中。使用 strncpy 复制到长度 strlen(item) + 1，确保包含字符串结束符。
// genv[++index] = nullptr;：

// 在数组中下一个位置设置为 nullptr，标志着环境变量的结束。

void InitEnv()
{
    extern char **environ;
    int index = 0;
    while (environ[index])
    {
        genv[index] = (char *)malloc(strlen(environ[index]) + 1);
        strncpy(genv[index], environ[index], strlen(environ[index]) + 1);
        index++;
    }
    genv[index] = nullptr;
}

// 功能
// 初始化环境变量：该函数从系统环境变量中读取所有环境变量并将其存储在 genv 数组中。
// 逐行分析
// extern char **environ;：声明外部变量 environ，这是一个指向环境变量字符串数组的指针，通常在 C/C++ 中可用。

// int index = 0;：初始化索引为 0，用于遍历 environ。

// while(environ[index])：遍历 environ 数组，直到找到第一个空指针，表示所有环境变量都已读取。

// genv[index] = (char*)malloc(strlen(environ[index] + 1));：

// 为每个环境变量分配内存。这里 strlen(environ[index] + 1) 计算字符串的长度，并加 1 来包含 \0 结束符。这里有一个小错误，应该是 strlen(environ[index]) + 1，而不是 strlen(environ[index] + 1)。
// strncpy(genv[index], environ[index], strlen(environ[index] + 1));：

// 将 environ[index] 的内容复制到 genv[index] 中。这里同样存在一个错误，应该是 strncpy(genv[index], environ[index], strlen(environ[index]) + 1);。
// index++;：增加索引，以便处理下一个环境变量。

// genv[index] = nullptr;：在数组最后设置 nullptr，表示环境变量的结束。

// 内建命令处理
// shell自己执行命令, 本质是shell调用自己的函数
//  功能：检查并执行内建命令，如cd、export、env和echo，处理相应的参数和逻辑。
bool CheckAndExecBuiltCommand()
{
    if (strcmp(gargv[0], "cd") == 0)
    {
        // 内建命令
        if (gargc == 2)
        {
            chdir(gargv[1]);
            lastcode = 0;
        }
        else
        {
            lastcode = 1;
        }
        return true;
    }
    else if (strcmp(gargv[0], "export") == 0)
    {
        // export也是内建命令
        if (gargc == 2)
        {
            AddEnv(gargv[1]);
            lastcode = 0;
        }
        else
        {
            lastcode = 2;
        }
        return true;
    }
    else if (strcmp(gargv[0], "env") == 0)
    {
        for (int i = 0; genv[i]; i++)
        {
            printf("%s\n", genv[i]);
        }
        lastcode = 0;
        return true;
    }
    else if (strcmp(gargv[0], "echo") == 0)
    {
        if (gargc == 2)
        {
            // echo $?
            // echo $PATH
            // echo hello
            if (gargv[1][0] == '$')
            {
                if (gargv[1][1] == '?')
                {
                    printf("%d\n", lastcode);
                    lastcode = 0;
                }
            }
            else
            {
                printf("%s\n", gargv[1]);
                lastcode = 0;
            }
        }
        else
        {
            lastcode = 3;
        }
        return true;
    }
    return false;
}

// 功能：用于更改当前工作目录。
// 逻辑：
// 检查参数个数 gargc 是否等于 2（即命令和目录）。
// 如果参数正确，调用 chdir 函数改变目录，并将 lastcode 设置为 0，表示成功。
// 如果参数不正确，将 lastcode 设置为 1，表示错误。

// 功能：用于设置或导出环境变量。
// 逻辑：
// 同样检查参数个数 gargc 是否为 2。
// 如果正确，调用 AddEnv 函数添加环境变量，并将 lastcode 设置为 0。
// 如果参数不正确，设置 lastcode 为 2。

// 功能：列出所有环境变量。
// 逻辑：
// 遍历 genv 数组，打印每个环境变量。
// 将 lastcode 设置为 0。

// 功能：输出字符串或变量。
// 逻辑：
// 检查参数个数是否为 2。
// 如果是，检查 gargv[1] 的第一个字符是否为 $：
// 如果是 $?，输出 lastcode 的值。
// 如果是其他以 $ 开头的字符串，直接输出这个字符串（假设它是环境变量）。
// 如果没有 $，则将 lastcode 设置为 3。
// 如果参数个数不正确，返回 false。

// 使用内建命令的好处
//  快速响应：内建命令通常直接在 shell 进程内执行，无需创建新的进程，减少了上下文切换的开销。这使得内建命令的执行速度更快。
//  基本功能：内建命令提供了一些与 shell 紧密相关的基本功能，如改变目录（cd）、设置环境变量（export）、获取当前环境（env）等，这些操作通常与 shell 的状态直接相关。
//  环境影响：某些内建命令（如 cd）会影响 shell 的状态（例如当前工作目录），如果用外部程序实现，改变的状态不会反映到 shell 中。
//  简化用户操作：用户可以直接使用内建命令而无需担心路径问题。例如，cd 命令只需要提供目标路径，而不必寻找相应的外部可执行文件。
//  状态保持：内建命令如 echo 和 export 能够直接访问 shell 的状态和环境变量，而不需要通过外部程序来管理，这样可以更好地处理如 $?（上一个命令的退出状态）这样的特殊情况。

// 主循环
//  功能：主程序循环，负责显示提示符、获取用户命令、解析命令、检查内建命令，并执行命令。

int main()
{
    InitEnv();
    char command_buffer[basesize];
    while (true)
    {
        PrintCommandLine(); // 1. 命令行提示符
        // command_buffer -> output
        if (!GetCommandLine(command_buffer, basesize)) // 2. 获取用户命令
        {
            continue;
        }
        // printf("%s\n", command_buffer);
        // ls
        //"ls -a -b -c -d"->"ls" "-a" "-b" "-c" "-d"
        ParseCommandLine(command_buffer, strlen(command_buffer)); // 3. 分析命令

        if (CheckAndExecBuiltCommand())
        {
            continue;
        }

        ExecuteCommand(); // 4. 执行命令
    }
    return 0;
}